﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="SelectorSynchronizationBehavior.cs" company="JDML Consulting Ltd.">
//   Copyright 2011 Photon Project - http://photon.codeplex.com
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace Photon.Windows.Controls
{
    using System.Collections.Specialized;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Controls.Primitives;
    using System.Windows.Data;
    using System.Windows.Interactivity;

    /// <summary>
    /// Synchronizes with <see cref="Selector"/> types to work around null reference issues.
    /// </summary>
    public class SelectorSynchronizationBehavior : Behavior<Selector>
    {
        #region Fields

        public static readonly DependencyProperty SelectedItemPathProperty =
            DependencyProperty.Register("SelectedItemPath", typeof(string), 
                typeof(SelectorSynchronizationBehavior), new PropertyMetadata(null, SelectedItemPathChanged));

        public static readonly DependencyProperty SelectedValuePathProperty =
            DependencyProperty.Register("SelectedValuePath", typeof(string), 
                typeof(SelectorSynchronizationBehavior), new PropertyMetadata(null, SelectedValuePathChanged));

        public static readonly DependencyProperty IsSelectedItemMappedProperty =
            DependencyProperty.Register("IsSelectedItemMapped", typeof(bool), 
                typeof(SelectorSynchronizationBehavior), new PropertyMetadata(false, IsSelectedItemMappedChanged));

        private static readonly DependencyProperty SelectedItemProperty =
            DependencyProperty.Register("SelectedItem", typeof(object), 
                typeof(SelectorSynchronizationBehavior), new PropertyMetadata(null, SelectedItemChanged));

        private readonly BindingEvaluator<object> _selectedValueEvaluator = new BindingEvaluator<object>();

        private bool _isSynchronizationPending;

        #endregion

        private object SelectedItem
        {
            get { return GetValue(SelectedItemProperty); }
        }

        private bool IsOutOfSync
        {
            get
            {
                var selectedItem = AssociatedObject.SelectedItem;
                if (IsSelectedItemMapped)
                {
                    selectedItem = GetSelectedValue(AssociatedObject.SelectedItem);
                }

                return !Equals(selectedItem, SelectedItem) || (AssociatedObject.SelectedIndex == -1 && selectedItem == null);
            }
        }

        /// <summary>
        /// Gets or sets the <see cref="Binding "/> used to synchronize the current selection
        /// </summary>
        public string SelectedItemPath
        {
            get { return (string)GetValue(SelectedItemPathProperty); }
            set { SetValue(SelectedItemPathProperty, value); }
        }

        /// <summary>
        /// Gets or sets the path that matches selected items.
        /// </summary>
        public string SelectedValuePath
        {
            get { return (string)GetValue(SelectedValuePathProperty); }
            set { SetValue(SelectedValuePathProperty, value); }
        }

        /// <summary>
        /// Gets or sets a value indicating whether this instance is selected item mapped.
        /// </summary>
        /// <value><c>true</c> if this instance is selected item mapped; otherwise, <c>false</c>.</value>
        public bool IsSelectedItemMapped
        {
            get { return (bool)GetValue(IsSelectedItemMappedProperty); }
            set { SetValue(IsSelectedItemMappedProperty, value); }
        }

        private static void SelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((SelectorSynchronizationBehavior)d).SelectedItemChanged(e.OldValue, e.NewValue);
        }

        private static void IsSelectedItemMappedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((SelectorSynchronizationBehavior)d).IsSelectedItemMappedChanged((bool)e.OldValue, (bool)e.NewValue);
        }

        private static void SelectedValuePathChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((SelectorSynchronizationBehavior)d).SelectedValueChanged((string)e.OldValue, (string)e.NewValue);
        }

        private static void SelectedItemPathChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((SelectorSynchronizationBehavior)d).SelectedItemPathChanged((string)e.OldValue, (string)e.NewValue);
        }

        private void SelectedValueChanged(string oldValue, string newValue)
        {
            _selectedValueEvaluator.ValueBinding = new Binding(newValue);
        }

        private void SelectedItemChanged(object oldValue, object newValue)
        {
            Synchronize();
        }

        private void SelectedItemPathChanged(string oldValue, string newValue)
        {
            BindingOperations.SetBinding(this, SelectedItemProperty, new Binding(newValue) { Mode = BindingMode.TwoWay });
        }

        private void IsSelectedItemMappedChanged(bool oldValue, bool newValue)
        {
            Synchronize();
        }

        /// <summary>
        /// Indexes the of item.
        /// </summary>
        /// <param name="item">The item.</param>
        /// <returns>The index of the specified item if it exists within the selectors items collection; otherwise -1.</returns>
        public int IndexOfItem(object item)
        {
            if (!IsSelectedItemMapped)
            {
                item = GetSelectedValue(item);
            }

            for (var i = 0; i < AssociatedObject.Items.Count; i++)
            {
                var listItem = AssociatedObject.Items[i];
                if (listItem == null)
                {
                    if (item == null)
                    {
                        return i;
                    }
                }
                else if (item != null && Equals(item, GetSelectedValue(listItem)))
                {
                    return i;
                }
            }
            return -1;
        }

        private object GetSelectedValue(object item)
        {
            return string.IsNullOrEmpty(SelectedValuePath)
                ? item
                : _selectedValueEvaluator.GetValueFromContext(item);
        }

        protected override void OnAttached()
        {
            base.OnAttached();

            // listen for selection change notifications
            AssociatedObject.SelectionChanged += SelectorSelectionChanged;

            // listen to collection change notifications, so we can sync selection when items are loaded
            var collectionChanged = AssociatedObject.Items as INotifyCollectionChanged;
            if (collectionChanged != null)
            {
                collectionChanged.CollectionChanged += ItemsChanged;
            }

            Synchronize();
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();
            AssociatedObject.SelectionChanged -= SelectorSelectionChanged;

            // listen to collection change notifications, so we can sync selection when items are loaded
            var collectionChanged = AssociatedObject.Items as INotifyCollectionChanged;
            if (collectionChanged != null)
            {
                collectionChanged.CollectionChanged -= ItemsChanged;
            }
        }

        private void ItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (_isSynchronizationPending || !IsOutOfSync)
            {
                return;
            }
            
            // must invoke on dispatcher as not doing so really screws up the Selector :(
            _isSynchronizationPending = true;
            Dispatcher.BeginInvoke(Synchronize);
        }

        private void Synchronize()
        {
            if (AssociatedObject == null)
            {
                return;
            }

            _isSynchronizationPending = false;

            // synchronize
            if (IsOutOfSync)
            {
                AssociatedObject.SelectedIndex = IndexOfItem(SelectedItem);
            }
        }

        private void SelectorSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            // get the actual value selected
            var item = GetUnderlyingItem(AssociatedObject.SelectedItem);
            if (IsSelectedItemMapped)
            {
                item = GetSelectedValue(item);
            }
            SetValue(SelectedItemProperty, item);
        }

        /// <summary>
        /// Gets the underlying value associated with a <see cref="ComboBox"/> item.
        /// </summary>
        /// <param name="item">
        /// The item whose underlying value should be returned.
        /// </param>
        /// <returns>
        /// The underlying value of the supplied item.
        /// </returns>
        private static object GetUnderlyingItem(object item)
        {
            // item "should" be the value of the DataContext however, for nulls we get the actual ComboBoxItem, WTF?
            var container = item as ListBoxItem;
            return container != null ? container.DataContext : item;
        }
    }
}